<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/global.css">

    <style>
        .scene {
            width: 100vw;
            height: 100vh;
            background-image: url('./assets/poker/background.png');
            user-select: none;
            width: 100%;
        }

        .card-table {
            position: absolute;
            width: 100%;
            transform: translate(-50%, -50%);
            top: 50%;
            left: 50%;
            display: flex;
            justify-content: center;
        }


        .player {
            position: absolute;
            bottom: 60px;
            width: 100%;
        }


        .player-hold-cards {
            display: flex;
            justify-content: flex-end;
            padding-right: 20vmin;

        }

        .player-name {
            position: absolute;
            bottom: -30px;
            left: 40px;
        }

        .card-item.selected img {
            transform: translateY(-20px);
        }

        .robot {
            top: 30px;
            position: absolute;
            width: 100%;
        }

        .robot-hold-cards {
            display: flex;
            padding-left: 10vmin;
        }

        .robot-name {
            position: absolute;
            top: 170px;
            right: 40px;
        }

        .card-item {
            max-width: 10vmin;
            display: flex;
        }


        .player-operation-buttons {
            position: absolute;
            bottom: 180px;
            width: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .player-operation-buttons>div {
            display: flex;
        }

        .hidden {
            display: none;
        }


        [class$='btn'] {
            display: inline-flex;
            padding: 0 20px 3px;
            line-height: 40px;
            background: linear-gradient(to bottom, rgb(87, 196, 245), rgb(26, 147, 206));
            color: rgb(254, 252, 255);
            cursor: pointer;
            border-radius: 4px;
            font-weight: bold;
            box-shadow: inset 0px -3px 0 rgb(19, 98, 139);
        }

        [class$='btn']+[class$='btn'] {
            margin-left: 50px;
        }


        [class$='btn']:active {
            opacity: .7;
            box-shadow: inset 0px 0px 0 transparent;
        }

        .pass-btn {
            background: linear-gradient(to bottom, rgb(245, 87, 87), rgb(206, 26, 26));
            box-shadow: inset 0px -3px 0 rgb(139, 19, 39);
        }

        .powerless {
            font-size: 28px;
            font-weight: bold;
            color: rgb(218, 183, 96);
        }

        .non-compliance {
            font-size: 28px;
            font-weight: bold;
            color: rgb(218, 183, 96);
        }
    </style>
</head>

<body>
    <div class="scene">
        <div class="robot">
            <div class="robot-name">玩家嘻嘻嘻xxxx</div>
            <div class="robot-hold-cards">
                <div class="card-item">
                    <img src="./assets/poker/背面.jpg">
                </div>
                <div class="card-item">
                    <img src="./assets/poker/背面.jpg">
                </div>
                <div class="card-item">
                    <img src="./assets/poker/背面.jpg">
                </div>
                <div class="card-item">
                    <img src="./assets/poker/背面.jpg">
                </div>
                <div class="card-item">
                    <img src="./assets/poker/背面.jpg">
                </div>
            </div>
        </div>


        <div class="card-table">
            <div class="card-item">
                <img src="./assets/poker/背面.jpg">
            </div>
            <div class="card-item">
                <img src="./assets/poker/背面.jpg">
            </div>
            <div class="card-item">
                <img src="./assets/poker/背面.jpg">
            </div>
            <div class="card-item">
                <img src="./assets/poker/背面.jpg">
            </div>
            <div class="card-item">
                <img src="./assets/poker/背面.jpg">
            </div>
        </div>


        <div class="player">
            <div class="player-operation-buttons">
                <div>
                    <div class="dis-btn">出牌</div>
                    <div class="pass-btn">不要</div>
                </div>
                <div>
                    <div class="powerless">没有能大过的牌</div>
                    <div class="non-compliance">不符合规则</div>
                </div>
            </div>
            <div class="player-name">玩家嘻嘻嘻xxxx</div>
            <div class="player-hold-cards">
                <div class="card-item" data-index="0"> </div>
                <div class="card-item" data-index="1"> </div>
                <div class="card-item" data-index="2"> </div>
                <div class="card-item" data-index="3"> </div>
                <div class="card-item" data-index="4"> </div>
            </div>
        </div>

    </div>


    <script type="module">
        import { Randoms } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/randoms.js";

        /**
         * 打组
         * @author 	 linyisonger
         * @date 	 2025-03-15
         */
        function groupBy(arr, func) {
            const tmpDict = {}
            for (let i = 0; i < arr.length; i++) {
                if (!tmpDict[func(arr[i], i)]) tmpDict[func(arr[i], i)] = []
                tmpDict[func(arr[i], i)].push(arr[i])
            }
            return tmpDict
        }

        /**  
            游戏准备
            牌数与人数:
                使用一副54张的扑克牌，至少2人参与，最多可容纳5人。
                发牌:开始每人发5张牌。
            牌型及大小
                单牌:大小顺序为7>大王>小王>5>2>3>A>K>Q>J>10>9>8>6>4.同等点数的牌，黑桃>红桃>梅花>方块。
                对牌:数值相同的两张牌，大小王算一对。点数一样时，带黑桃的一方大。
                三条:单独三张点数一样的牌，不带牌，三带对:数值相同的三张牌，带两张一样的。别的三张要大过它必须也带三张一样的，带的牌不参与比大小。
                四条:四张点数一样的牌，
                四带一:数值相同的四张牌，带一张单张。别的四张要大过它必须也带一张，带的牌不参与比大小。
            游戏流程
                首轮出牌:由手上最小牌的人先出，可以出包含最小牌的任意牌型。
                后续出牌:按逆时针顺序依次出牌，轮到玩家跟牌时，玩家需出比上一个玩家大的牌或选择“不出”。除炸弹外，先出牌者出多少张牌，后面的人就需要跟多少张来压牌。
                补牌:当一轮出牌结束后，由最后出牌且牌最大的玩家开始补牌，直到手中的牌数再次达到5张，然后按照逆时针顺序，其他玩家也依次补牌。
            得分与胜负
                得分:5代表5分，10和K代表10分。当一轮中出分牌后，最后牌最大的玩家可获得本轮所有分牌的分数。
                胜负:通常最先出完牌的玩家算赢;或者游戏结束后，得分最高的玩家获胜。
         */
        const CARD_TYPE = {
            2: '2',
            3: '3',
            4: '4',
            5: '5',
            6: '6',
            7: '7',
            8: '8',
            9: '9',
            10: '10',
            J: 'J',
            Q: 'Q',
            K: 'K',
            A: 'A',
            LITTLE_JOKER: '小王',
            BIG_JOKER: '大王',
            BACK: '背面'
        }
        const CARD_SUITS = {
            DIAMOND: '方片',
            HEART: '红桃',
            CLUB: '梅花',
            SPADE: '黑桃'
        }
        // 7>大王>小王>5>2>3>A>K>Q>J>10>9>8>6>4 
        const TYPE_LEVEL = {
            [CARD_TYPE[4]]: 1,
            [CARD_TYPE[6]]: 2,
            [CARD_TYPE[8]]: 3,
            [CARD_TYPE[9]]: 4,
            [CARD_TYPE[10]]: 5,
            [CARD_TYPE.J]: 6,
            [CARD_TYPE.Q]: 7,
            [CARD_TYPE.K]: 8,
            [CARD_TYPE.A]: 9,
            [CARD_TYPE[3]]: 10,
            [CARD_TYPE[2]]: 11,
            [CARD_TYPE[5]]: 12,
            [CARD_TYPE.LITTLE_JOKER]: 13,
            [CARD_TYPE.BIG_JOKER]: 14,
            [CARD_TYPE[7]]: 15,
        }
        // 黑桃>红桃>梅花>方块
        const SUITS_LEVEL = {
            [CARD_SUITS.DIAMOND]: 1,
            [CARD_SUITS.CLUB]: 2,
            [CARD_SUITS.HEART]: 3,
            [CARD_SUITS.SPADE]: 4
        }
        // 牌型
        const GROUP_TYPE = {
            LEAFLETS: "单张",
            PAIRS: '一对',
            TRIPLE: '三张',
            FOUR: '四张',
            TRIPLE_WITH_PAIRS: '三带二',
            FOUR_WITH_SINGLE: '四带一',
        }

        const PLAYER_STATUS = {
            AUTO: '托管',
            MANUAL: '手动'
        }

        class EventBus {
            constructor() {
                // 初始化事件列表
                this.eventObject = {};
                // 回调函数列表的id
                this.callbackId = 0;
            }
            // 发布事件
            publish(eventName, ...args) {
                // 取出当前事件所有的回调函数
                const callbackObject = this.eventObject[eventName];

                if (!callbackObject) return console.warn(eventName + " not found!");

                // 执行每一个回调函数
                for (let id in callbackObject) {
                    // 执行时传入参数
                    callbackObject[id](...args);
                }
            }
            // 订阅事件
            subscribe(eventName, callback) {
                // 初始化这个事件
                if (!this.eventObject[eventName]) {
                    // 使用对象存储，注销回调函数的时候提高删除的效率
                    this.eventObject[eventName] = {};
                }

                const id = this.callbackId++;

                // 存储订阅者的回调函数
                // callbackId使用后需要自增，供下一个回调函数使用
                this.eventObject[eventName][id] = callback;

                // 每一次订阅事件，都生成唯一一个取消订阅的函数
                const unSubscribe = () => {
                    // 清除这个订阅者的回调函数
                    delete this.eventObject[eventName][id];

                    // 如果这个事件没有订阅者了，也把整个事件对象清除
                    if (Object.keys(this.eventObject[eventName]).length === 0) {
                        delete this.eventObject[eventName];
                    }
                };

                return { unSubscribe };
            }
        }



        class Card {
            constructor(type = '', suits = '') {
                this.type = type;
                this.suits = suits;
            }
            get url() {
                return `./assets/poker/${this.type}${this.suits && '-' + this.suits}.jpg`
            }


            /**
             * 卡片对比
             * A < B True
             * A > B Fasle
             * @author 	 linyisonger
             * @date 	 2025-03-14
             * @param {Card} a
             * @param {Card} b
             */
            static comparison(a, b) {
                if (!a || !b) return false
                if (TYPE_LEVEL[a.type] < TYPE_LEVEL[b.type]) return true;  // A牌面小于B牌面 True
                if (TYPE_LEVEL[a.type] > TYPE_LEVEL[b.type]) return false; // A牌面大于B牌面 False
                if (SUITS_LEVEL[a.suits] < SUITS_LEVEL[b.suits]) return true; // A牌面等于B牌面 且A花色小于B花色 True
                return false;  // 其他 False
            }


            /**
             * 卡片类型
             * A < B True
             * A > B False
             * @author 	 linyisonger
             * @date 	 2025-03-15
             */
            static comparisonByType(a, b) {
                return TYPE_LEVEL[a] < TYPE_LEVEL[b]
            }

            /**
             * 卡牌对比
             * A < B True
             * A > B False
             * @author 	 linyisonger
             * @date 	 2025-03-15
             * @param {Card[]} a
             * @param {Card[]} b
             */
            static comparisonByCards(a, b) {
                let groupType = Card.groupType(a)
                switch (groupType) {
                    case GROUP_TYPE.LEAFLETS:
                    case GROUP_TYPE.PAIRS:
                    case GROUP_TYPE.FOUR:
                    case GROUP_TYPE.TRIPLE:
                        return Card.comparison(a[0], b[0])
                    case GROUP_TYPE.TRIPLE_WITH_PAIRS:
                    case GROUP_TYPE.FOUR_WITH_SINGLE:
                        let aGroup = groupBy(a, c => c.type)
                        let bGroup = groupBy(b, c => c.type)
                        let aCard = null
                        let bCard = null
                        for (const key in aGroup) if (aGroup[key].length >= 3) aCard = aGroup[key][0]
                        for (const key in bCard) if (bCard[key].length >= 3) bCard = bCard[key][0]
                        return Card.comparison(aCard, bCard)
                }
            }


            /**
             * 数组中最小卡牌下表
             * @author 	 linyisonger
             * @date 	 2025-03-14
             * @param {Card[]} arr
             */
            static minCardIndex(arr) {
                if (arr.length === 0) return -1; // 处理空数组
                let idx = 0;
                for (let i = 1; i < arr.length; i++) if (Card.comparison(arr[i], arr[idx])) idx = i;
                return idx;
            }

            /**
             * 卡牌组类型
             * @author 	 linyisonger
             * @date 	 2025-03-15
             * @param {Card[]} cards
             */
            static groupType(cards) {
                let tmpGroup = Object.values(groupBy(cards, c => c.type))
                if (cards.length === 1) return GROUP_TYPE.LEAFLETS;
                if (cards.length === 2 && tmpGroup.length === 1) return GROUP_TYPE.PAIRS;
                if (cards.length === 3 && tmpGroup.length === 1) return GROUP_TYPE.TRIPLE;
                if (cards.length === 4 && tmpGroup.length === 1) return GROUP_TYPE.FOUR;
                if (cards.length === 5 && tmpGroup.length === 2) {
                    if (tmpGroup.filter(g => g.length === 3)) return GROUP_TYPE.TRIPLE_WITH_PAIRS
                    if (tmpGroup.filter(g => g.length === 4)) return GROUP_TYPE.FOUR_WITH_SINGLE
                }
                return null;
            }

            /**
             * 提示
             * 
             * hold > target 的牌型
             * 
             * @author 	 linyisonger
             * @date 	 2025-03-15
             * @param {Card[]} hold
             * @param {Card[]} target
             * @returns {Card[]}
             */
            static hint(hold, target) {
                let groupType = Card.groupType(target) // 目标牌型
                let holdGroup = groupBy(hold, c => c.type) // 持有打组
                let groupTypeNum = {
                    [GROUP_TYPE.LEAFLETS]: 1,
                    [GROUP_TYPE.PAIRS]: 2,
                    [GROUP_TYPE.TRIPLE]: 3,
                    [GROUP_TYPE.FOUR]: 4,
                    [GROUP_TYPE.FOUR_WITH_SINGLE]: 5,
                    [GROUP_TYPE.TRIPLE_WITH_PAIRS]: 5
                }[groupType]   // 牌型张数


                if (hold.length < groupTypeNum) return [] // 手牌数小于牌型数 
                if (groupType === GROUP_TYPE.LEAFLETS) {  // 单张
                    for (let i = hold.length - 1; i >= 0; i--)  if (Card.comparison(target[0], hold[i])) return [hold[i]]
                }
                else if (groupType === GROUP_TYPE.PAIRS) { // 一对
                    // 是否有黑桃
                    let hasSpade = target.some(c => c.suits === CARD_SUITS.SPADE)
                    // 单张卡牌类型
                    let type = target[0].type
                    for (const key in holdGroup) {
                        if (holdGroup[key].length >= groupTypeNum) { // 张数大于等于2
                            if (Card.comparisonByType(key, type)) delete holdGroup[key] // 当key < type 时
                            if (hasSpade && key === type) delete holdGroup[key] // 相同
                        }
                        else delete holdGroup[key]
                    }
                    // 排序后取最小的组合的前两张
                    let keys = Object.keys(holdGroup)
                    // 排序
                    if (keys.length > 1) keys.sort((a, b) => Card.comparisonByType(a, b) ? 1 : -1)
                    if (keys.length > 0) return holdGroup[keys[0]].slice(0, 2)
                }
                else if (groupType === GROUP_TYPE.TRIPLE || groupType === GROUP_TYPE.FOUR) { // 三张 or 四张
                    // 单张卡牌类型
                    let type = target[0].type
                    for (const key in holdGroup) {
                        if (holdGroup[key].length >= groupTypeNum) { // 张数大于等于 牌型张数
                            if (Card.comparisonByType(key, type)) delete holdGroup[key] // 当key < type 时 
                        }
                        else delete holdGroup[key]
                    }

                    let keys = Object.keys(holdGroup)
                    if (keys.length > 0) return holdGroup[keys[0]].slice(0, groupTypeNum)
                }
                else if (groupType === GROUP_TYPE.TRIPLE_WITH_PAIRS || groupType === GROUP_TYPE.FOUR_WITH_SINGLE) {
                    if (!Card.comparisonByCards(hold, target) && Card.groupType(hold) === groupType) return hold
                }
                return []
            }
        }

        class Round {
            /** @type {Record[]} 所有记录*/
            allRecord = []

            /** @type {Record} 最后记录*/
            get lastRecord() {
                return this.allRecord.at(-1)
            }

            /** @type {Card[]} 当前牌面*/
            get lastCards() {
                return this.lastRecord?.cards || []
            }

            /**
             * @param {Game} game
             */
            constructor(game) {
                this.currentGame = game;
            }

            /**
             * 结束
             */
            get end() {
                // 记录数量小于玩家的人数 不可能结束
                if (this.allRecord.length < this.currentGame.joinPlayers.length) return false
                // 记录倒着数玩家数量-1数量的记录  如都是pass则结束
                return this.allRecord.slice(this.allRecord.length - this.currentGame.joinPlayers.length + 1).every(r => r.pass)
            }

        }

        class Record {
            /** @type {Player} 玩家*/
            player = null
            /** @type {Card[]} 牌面*/
            cards = []

            constructor(player, cards) {
                this.player = player;
                this.cards = cards;
                console.log(`${player.name} 打出 ${this.groupType} `, cards);
                uiEvent.publish('updateCardTable', cards)
            }

            get pass() {
                return this.cards.length === 0
            }

            get groupType() {
                return Card.groupType(this.cards)
            }
        }

        class Player {
            // 玩家状态
            status = PLAYER_STATUS.AUTO

            /**
             * @param {Game} game
             */
            constructor(game) {
                this.currentGame = game;
            }
            /** @type {string} ID */
            id = Randoms.uuid()
            /** @type {string} 名称*/
            get name() {
                return '玩家' + this.id.slice(0, 8)
            }
            /** @type {Card[]} 持有手牌*/
            holdCards = []

            /**
             * 持有组合
             * @type {{[key:string]:Card[]}}
             */
            get holdGroup() {
                return groupBy(this.holdCards, c => c.type)
            }

            // 最大持有手牌数
            maxHoldCardsCount = 5

            /**
             * 出牌
             * @param {Round} round
             */
            discard(round) {
                console.log('discard');
                if (this.status === PLAYER_STATUS.AUTO) {
                    console.log('自动', round.lastCards);
                    let cards = this.betterCards(round.lastCards) // 最小能大上的牌 
                    this.holdCards = this.holdCards.filter(h => !cards.includes(h)) // 出牌操作 
                    round.allRecord.push(new Record(this, cards))  // 一条记录
                    this.currentGame.nextPlayerDiscard() // 下一个人 
                    uiEvent.publish('updateRobotHoldCards', this.holdCards) // 轮到玩家出牌
                }
                else {
                    uiEvent.publish('showPlayerOperationButtons') // 轮到玩家出牌 
                }
            }
            /**
             * 起牌 
             */
            pickup() {
                let pickupCount = this.maxHoldCardsCount - this.holdCards.length // 需要起几张牌
                for (let i = 0; i < pickupCount; i++) {
                    let card = this.currentGame.randomlyDrawCards();
                    card && this.holdCards.push(card)
                }
                this.holdCards.sort((a, b) => Card.comparison(a, b) ? 1 : -1);
            }

            /**
             * 最小卡牌
             * @type {Card}
             */
            get holdMinCard() {
                return this.holdCards.at(-1);
            }

            /**
             * 是否胜过的组合
             * @author 	 linyisonger
             * @date 	 2025-03-15
             * @param {Card[]} cards
             * 
             */
            betterCards(cards) {
                // 组类型
                let groupType = Card.groupType(cards)
                // 自己先出牌 最小的组合
                if (groupType === null) return this.holdCards.filter(c => c.type === this.holdMinCard.type)
                return Card.hint(this.holdCards, cards)
            }

        }

        class Game {

            /** @type {Card[]} 所有卡牌*/
            allCards = generateDeckOfCards();
            /** @type {Round[]} 所有回合*/
            allRounds = []
            /** @type {Player[]} 加入玩家*/
            joinPlayers = [new Player(this), new Player(this)]

            // 当前玩家索引
            currentPlayerIndex = 0;
            // 当前玩家
            get currentPlayer() {
                return this.joinPlayers[this.currentPlayerIndex]
            }

            // 当前回合
            currentRound = new Round();

            /**
             * 下一个玩家
             * 切换玩家进行出牌操作
             */
            nextPlayer = () => {
                this.currentPlayerIndex++
                if (this.currentPlayerIndex >= this.joinPlayers.length) this.currentPlayerIndex = 0
            }


            /**
             * 下一个玩家出牌
             */
            nextPlayerDiscard = () => {
                // 当手牌为0且牌堆为0时 游戏结束
                if (this.currentPlayer.holdCards.length === 0 && this.allCards.length === 0) {
                    console.log('游戏结束');
                    return;
                }
                console.log("nextPlayerDiscard");
                this.nextPlayer(); // 下一个玩家
                if (this.currentRound.end) { // 检测回合是否结束
                    console.log('检测回合是否结束');
                    uiEvent.publish('updateCardTable', [])
                    this.allRounds.push(this.currentRound);
                    this.nextRound();
                }
                else this.currentPlayer.discard(this.currentRound)
            }


            /**
             * 下一回合
             * 切换回合
             */
            nextRound = async () => {
                this.currentRound = new Round(this)

                // 玩家起牌
                console.log('玩家起牌');

                for (let i = 0; i < this.joinPlayers.length; i++) {
                    this.currentPlayer.pickup();
                    if (this.currentPlayer.status === PLAYER_STATUS.AUTO) uiEvent.publish('updateRobotBaseInfo', this.currentPlayer.holdCards)
                    if (this.currentPlayer.status === PLAYER_STATUS.MANUAL) uiEvent.publish('updatePlayerHoldCards', this.currentPlayer.holdCards)
                    this.nextPlayer();
                }



                if (this.allRounds.length === 0) {
                    console.log('最小牌面对比');
                    // 牌面最小
                    let minCards = this.joinPlayers.map(p => p.holdMinCard)
                    let minCardIndex = Card.minCardIndex(minCards);
                    this.currentPlayerIndex = minCardIndex;

                    // 定制化变更
                    this.currentPlayer.status = PLAYER_STATUS.MANUAL

                    uiEvent.publish('updatePlayerHoldCards', this.currentPlayer.holdCards)
                    uiEvent.publish('updatePlayerBaseInfo', this.currentPlayer)
                    uiEvent.publish('updateRobotBaseInfo', this.joinPlayers[(minCardIndex + 1) % this.joinPlayers.length])
                    uiEvent.subscribe('discard', (cards) => {
                        // 隐藏玩家操作按钮
                        uiEvent.publish('hidePlayerOperationButtons')
                        // 出牌操作
                        this.currentPlayer.holdCards = this.currentPlayer.holdCards.filter(h => !cards.find(c => c.type === h.type && c.suits === h.suits))
                        uiEvent.publish('updatePlayerHoldCards', this.currentPlayer.holdCards)
                        // 一条记录
                        this.currentRound.allRecord.push(new Record(this.currentPlayer, cards))
                        // 下一个出牌
                        this.nextPlayerDiscard()

                    })

                    console.log('玩家牌面最小', this.currentPlayer);
                }

                this.currentPlayer.discard(this.currentRound)
            }

            /**
             * 开始游戏
             * @author 	 linyisonger
             * @date 	 2025-03-14
             */
            start = () => {
                this.nextRound()
                return this;
            }

            /**
             * 随机抽取卡牌 
             */
            randomlyDrawCards = () => {
                if (this.allCards.length == 0) return null // 发牌结束
                let index = Randoms.int(0, this.allCards.length) // 随机卡牌
                return this.allCards.splice(index, 1)[0]
            }

        }

        class UIController {
            static get container() {
                return document.body.querySelector('.scene')
            }

            /**
             * 机器人容器
             * @author 	 linyisonger
             * @date 	 2025-03-16
             */
            static get robotContainer() {
                return UIController.container.querySelector('.robot')
            }

            /**
             * 玩家容器
             * @author 	 linyisonger
             * @date 	 2025-03-16
             */
            static get playerContainer() {
                return UIController.container.querySelector('.player')
            }


            /**
             * 牌桌
             * @author 	 linyisonger
             * @date 	 2025-03-16
             */
            static get cardTable() {
                return UIController.container.querySelector('.card-table')
            }


            /**
             * 牌桌上的牌
             * @author 	 linyisonger
             * @date 	 2025-03-18 
             */
            static get cardTableCards() {
                const cardItemDomList = UIController.cardTable.querySelectorAll('.card-item')
                return UIController.cardDomConvertToCard(cardItemDomList);
            }

            /**
             * 玩家手上的牌
             * @author 	 linyisonger
             * @date 	 2025-03-18
             */
            static get playerHoldCards() {
                const cardItemDomList = UIController.playerContainer.querySelectorAll('.card-item')
                return UIController.cardDomConvertToCard(cardItemDomList);
            }


            /**
             * 玩家选中的牌
             * @author 	 linyisonger
             * @date 	 2025-03-18
             */
            static get playerSelectedCards() {
                const cardItemDomList = UIController.playerContainer.querySelectorAll('.card-item.selected')
                return UIController.cardDomConvertToCard(cardItemDomList);
            }



            /**
             * 卡牌转换图像DOM
             * @author 	 linyisonger
             * @date 	 2025-03-17
             * @param {Card} card
             */
            static cardConvertToImg(card) {
                if (!card) return ''
                return `<img src="${card.url}"/>`
            }


            /**
            * 卡牌元素转换卡牌对象
            * @author 	 linyisonger
            * @date 	 2025-03-18
            * @param {NodeListOf<Element>} doms
            * @returns {Card[]}
            */
            static cardDomConvertToCard(doms) {
                const cards = []
                doms.forEach(item => {
                    let type = item.getAttribute('data-type');
                    let suits = item.getAttribute('data-suits');
                    if (type) cards.push(new Card(type, suits))
                })
                return cards;
            }


            /**
             * 更新托管手牌
             * @author 	 linyisonger
             * @date 	 2025-03-16
             * @param {Card[]} holdCards
             */
            static updateRobotHoldCards(holdCards) {
                const cardItemDomList = UIController.robotContainer.querySelectorAll('.card-item')
                for (let i = 0; i < cardItemDomList.length; i++) {
                    const cardItemDom = cardItemDomList.item(i)
                    const cardItem = holdCards[i]
                    if (cardItem) cardItemDom.classList.remove('hidden')
                    else cardItemDom.classList.add('hidden')
                }
            }

            /**
             * 更新自己手牌
             * @author 	 linyisonger
             * @date 	 2025-03-16
             * @param {Card[]} holdCards
             */
            static updatePlayerHoldCards(holdCards) {
                const cardItemDomList = UIController.playerContainer.querySelectorAll('.card-item')
                for (let i = 0; i < cardItemDomList.length; i++) {
                    const cardItemDom = cardItemDomList.item(i)
                    const cardItem = holdCards[i]
                    cardItemDom.setAttribute('data-type', cardItem?.type || '')
                    cardItemDom.setAttribute('data-suits', cardItem?.suits || '')
                    cardItemDom.innerHTML = UIController.cardConvertToImg(cardItem)
                }


            }


            /**
             * 更新玩家基础信息
             * @author 	 linyisonger
             * @date 	 2025-03-16
             * @param {Player} player
             */
            static updatePlayerBaseInfo(player) {
                UIController.playerContainer.querySelector('[class$="-name"]').textContent = player.name
            }


            /**
             * 更新对手基础信息
             * @author 	 linyisonger
             * @date 	 2025-03-16
             * @param {Player} robot
             */
            static updateRobotBaseInfo(robot) {
                UIController.robotContainer.querySelector('[class$="-name"]').textContent = robot.name
            }


            /**
             * 更新牌桌的牌
             * @author 	 linyisonger
             * @date 	 2025-03-16
             * @param {Card[]} cards
             */
            static updateCardTable(cards) {
                const cardItemDomList = UIController.cardTable.querySelectorAll('.card-item')
                for (let i = 0; i < cardItemDomList.length; i++) {
                    const cardItemDom = cardItemDomList.item(i)
                    const cardItem = cards[i]
                    cardItemDom.setAttribute('data-type', cardItem?.type || '')
                    cardItemDom.setAttribute('data-suits', cardItem?.suits || '')
                    cardItemDom.innerHTML = UIController.cardConvertToImg(cardItem)
                }
            }


            /**
             * 初始化手牌
             * @author 	 linyisonger
             * @date 	 2025-03-16
             */
            static initPlayerHoldCards() {
                const cardItemDomList = UIController.playerContainer.querySelectorAll('.card-item')
                const playerOperationButtons = UIController.playerContainer.querySelector('.player-operation-buttons')
                const disBtn = playerOperationButtons.querySelector('.dis-btn'); // 出牌
                const passBtn = playerOperationButtons.querySelector('.pass-btn'); // 不要
                const powerless = playerOperationButtons.querySelector('.powerless'); // 没有能大过的牌
                const nonCompliance = playerOperationButtons.querySelector('.non-compliance'); // 不符合规则

                cardItemDomList.forEach(cardItemDom => {
                    cardItemDom.addEventListener('click', function () {
                        cardItemDom.classList.toggle('selected')
                        let cardTableCards = UIController.cardTableCards  // 获取当前牌桌上的牌
                        let playerHoldCards = UIController.playerHoldCards // 获取自己当前的手牌
                        let playerSelectedCards = UIController.playerSelectedCards // 玩家选择的手牌

                        let playerSelectedGroupType = Card.groupType(playerSelectedCards) // 玩家选择的组类型
                        let cardTableGroupType = Card.groupType(cardTableCards) // 牌桌上的组类型

                        // 不符合规则
                        let irregular = playerSelectedGroupType == null && playerSelectedCards.length // 选择不为空 但组为空
                            || cardTableGroupType != null && playerSelectedGroupType != cardTableGroupType && playerSelectedCards.length // 牌桌组类型不为空 并且 组类型 不相同
                            || Card.comparisonByCards(playerSelectedCards, cardTableCards)  // 对比大小


                        if (irregular) {
                            disBtn.classList.add('hidden')
                            nonCompliance.classList.remove('hidden')
                        }
                        else {
                            nonCompliance.classList.add('hidden')
                            disBtn.classList.remove('hidden')
                        }

                    })
                })


            }


            /**
             * 初始化操作按钮
             * @author 	 linyisonger
             * @date 	 2025-03-17
             */
            static initPlayerOperationButtons() {
                const playerOperationButtons = UIController.playerContainer.querySelector('.player-operation-buttons')
                const disBtn = playerOperationButtons.querySelector('.dis-btn'); // 出牌
                const passBtn = playerOperationButtons.querySelector('.pass-btn'); // 不要
                const powerless = playerOperationButtons.querySelector('.powerless'); // 没有能大过的牌
                const nonCompliance = playerOperationButtons.querySelector('.non-compliance'); // 不符合规则

                powerless.classList.add('hidden');
                nonCompliance.classList.add('hidden');

                passBtn.addEventListener('click', () => {
                    uiEvent.publish('discard', [])
                })

                disBtn.addEventListener('click', function () {
                    let playerSelectedCards = UIController.playerSelectedCards // 玩家选择的手牌
                    uiEvent.publish('discard', playerSelectedCards)
                    UIController.playerContainer.querySelectorAll(`.card-item.selected`).forEach(selected => selected.classList.remove('selected'))
                })

            }

            /**
             * 展示玩家操作按钮
             * @author 	 linyisonger
             * @date 	 2025-03-17
             */
            static showPlayerOperationButtons() {
                console.log('showPlayerOperationButtons');
                const playerOperationButtons = UIController.playerContainer.querySelector('.player-operation-buttons')
                playerOperationButtons.classList.remove('hidden')

                let cardTableCards = UIController.cardTableCards  // 获取当前牌桌上的牌
                let playerHoldCards = UIController.playerHoldCards // 获取自己当前的手牌
                let hintCards = Card.hint(playerHoldCards, cardTableCards) // 推荐打出的手牌

                const disBtn = playerOperationButtons.querySelector('.dis-btn'); // 出牌
                const passBtn = playerOperationButtons.querySelector('.pass-btn'); // 不要
                const powerless = playerOperationButtons.querySelector('.powerless'); // 没有能大过的牌

                console.log(hintCards, playerOperationButtons);
                if (hintCards.length || cardTableCards.length == 0) {
                    disBtn.classList.remove('hidden')
                    powerless.classList.add('hidden')
                    if (cardTableCards.length) passBtn.classList.remove('hidden')
                    else passBtn.classList.add('hidden')
                }
                else {
                    disBtn.classList.add('hidden')
                    passBtn.classList.remove('hidden')
                    powerless.classList.remove('hidden')
                }
            }

            /**
             * 隐藏玩家操作按钮
             * @author 	 linyisonger
             * @date 	 2025-03-17
             */
            static hidePlayerOperationButtons() {
                console.log('hidePlayerOperationButtons');

                const playerOperationButtons = UIController.playerContainer.querySelector('.player-operation-buttons')
                playerOperationButtons.classList.add('hidden')
            }



        }

        const uiEvent = new EventBus()

        UIController.initPlayerHoldCards()
        UIController.initPlayerOperationButtons();

        uiEvent.subscribe('updateRobotHoldCards', UIController.updateRobotHoldCards)
        uiEvent.subscribe('updatePlayerHoldCards', UIController.updatePlayerHoldCards)
        uiEvent.subscribe('updateRobotBaseInfo', UIController.updateRobotBaseInfo)
        uiEvent.subscribe('updatePlayerBaseInfo', UIController.updatePlayerBaseInfo)
        uiEvent.subscribe('updateCardTable', UIController.updateCardTable)

        uiEvent.subscribe('showPlayerOperationButtons', UIController.showPlayerOperationButtons)
        uiEvent.subscribe('hidePlayerOperationButtons', UIController.hidePlayerOperationButtons)

        // 初始化操作
        uiEvent.publish('updateCardTable', [])
        uiEvent.publish('hidePlayerOperationButtons')


        /**
         * 生成一副牌
         * @author 	 linyisonger
         * @date 	 2025-03-04
         */
        function generateDeckOfCards() {
            let cards = []
            for (const t of Object.values(CARD_TYPE)) {
                if (t === CARD_TYPE.BACK) continue; // 背面
                else if (t === CARD_TYPE.BIG_JOKER || t === CARD_TYPE.LITTLE_JOKER) cards.push(new Card(t)) // 大小王
                else for (const s of Object.values(CARD_SUITS)) cards.push(new Card(t, s)) // 其他
            }
            return cards
        }

        console.log(new Game().start());
    </script>

</body>

</html>